//
//                                    alphabet.js
//
//      ----------------------------------------------------------------------
//      Long-winded and overly pedantic/vaguely condescending instructions for 
//      adding a new script go here:
//      ----------------------------------------------------------------------
//
//  This is the location for any javascript code related to adding and enabling new 
//  alphabets in AATEL. Before going further and adding another script, please
//  ensure the following:
//
//      I. Ideally, the script you are adding should be standardized under Unicode.
//     II. If it's not, check to see that it's semi-officially registered to the 
//         Private Use Area via the Conscript Unicode Registry, at:
//         http://www.evertype.com/standards/csur/
//    III. If it appears in either do not deviate from the given Unicode range when
//         mapping characters.
//     IV. If it isn't even in the Conscript registry, then at the very least ensure
//         that the codepoints you intend to use don't collide with a script that 
//         already is in the Registry or standardized in Unicode.
//
//  Functions which use codepoints that collide with either standard Unicode or parts
//  of the PUA already registered with the Conscript Registry will not be allowed.
//  Update your fonts to use unallocated parts of the PUA, and preferably register
//  with the Conscript Registry first.
//
//  STEP 1: Using one of the other JSON maps in this directory as a model, create a
//          mapping of ARPABET phonemes to characters in the script you intend to
//          add. If you must use multiple characters to represent a particular sound,
//          use a dummy character in the JSON map to stand-in for them. Refer to
//          [ http://en.wikipedia.org/wiki/Arpabet#Vowels ] for a mapping of ARPABET
//          phonemes to IPA pronunciations. Note the distinction between AH0 and
//          AH1 and AH2; these are different sounds. AH0 is the English schwa, the
//          others are the normal 'uh' sound. Some alphabets have a specific character
//          to represent the schwa.
//
//  STEP 2: Add a <menuitem> listing under the "aatelButtonContext" <menupopup>
//          element in the overlay.xul file. This makes your alphabet visible to
//          users for selection in the context menu.

var EXPORTED_SYMBOLS = ["alpha"];

//  STEP 3: Keeping in convention with the alphabets already listed, add your JSON 
//          map below.
var alpha = {
    init: function() {
        function readJSON(uri) {
          // https://developer.mozilla.org/En/XMLHttpRequest/Using_XMLHttpRequest#Using_XMLHttpRequest_from_JavaScript_modules_.2F_XPCOM.C2.A0components
          var req = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]  
                    .createInstance(Components.interfaces.nsIXMLHttpRequest);
          req.open('GET', uri, false);
          req.overrideMimeType("application/json");
          req.send(null);
          return req.responseText;
        }

//  STEP 4: Again keeping with existing convention, include your object below.
//          alphaMap is used to map alphabet names used internally and on the
//          AATEL button Right-Click context menu to their respective functions.
        this.alphaMap = {
            "Deseret": JSON.parse(readJSON("resource://alphabet/map_deseret.json")),
            "Shavian": JSON.parse(readJSON("resource://alphabet/map_shavian.json")),
        };

        function extend(destination, source) {
            for (var property in source) {
                if (source.hasOwnProperty(property)) {
                    destination[property] = source[property];
                }
            }
            return destination;
        };
        this.mainDict = extend(this.cmuDict, this.userDict);
        udict = alpha.getUserDict();
        var ioService = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
        udictURI = ioService.newFileURI(udict).prePath + ioService.newFileURI(udict).path;
        alpha.userDict    = JSON.parse( readJSON(udictURI) );
        alpha.cmuDict     = JSON.parse( readJSON("resource://dict/cmudict07a.json") );
    },

    // Check first in any dictionaries provided as an argument, then in the user
    // dictionary, then in cmuDict.
    lookup: function(word) {
        word = word.toUpperCase();
        function tryDict(dict) {
            ph = dict[word];
            if (ph) {
                return ph;
            }
            else {
                return null;
            }
        }
        var result;
        for (var i = 1; i < arguments.length; i++) {   
            if (result = tryDict(arguments[i])) return result;
        }
        if (result = tryDict(alpha.userDict)) return result;
        if (result = tryDict(alpha.cmuDict))  return result;
        return null;
    },
   
    
    // https://developer.mozilla.org/en/XUL_School/Local_Storage
    getUserDict: function() {  
        var aatelDir = Components.classes["@mozilla.org/file/directory_service;1"]
            .getService(Components.interfaces.nsIProperties)
            .get("ProfD", Components.interfaces.nsIFile);
        aatelDir.append("aatel");  
        if (!aatelDir.exists() || !aatelDir.isDirectory()) {  
            aatelDir.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0774);  
        }  
        aatelDir.append("userdict.json");
        var userDictionary = Components.classes["@mozilla.org/file/local;1"].  
            createInstance(Components.interfaces.nsILocalFile);  
        userDictionary.initWithPath(aatelDir.path);  

        return userDictionary;  
    },  


    writeDictionary: function(data) {
        var file = alpha.getUserDict();
        Components.utils.import("resource://gre/modules/NetUtil.jsm");
        Components.utils.import("resource://gre/modules/FileUtils.jsm");

        // file is nsIFile, data is a string
        var ostream = FileUtils.openSafeFileOutputStream(file, 0x02 | 0x08 | 0x10);
        var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
                        createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
        converter.charset = "UTF-8";
        var istream = converter.convertToInputStream(JSON.stringify(data));
        NetUtil.asyncCopy(istream, ostream);
    },

//------------------------------------------------------------------------------------
// The following section has been gutted thoroughly. Abbreviations and post-processing
// are now described in maps included with the alphabet json files, and the logic for them
// has been abstracted out into the transliterate() and transWord() functions. Comments
// have been left behind for reference. TODO: Move and update the information below.
//------------------------------------------------------------------------------------


//  STEP 5: Include your conversion function among the ones given below, keeping
//          in alphabetical order. Each function receives an array representing the 
//          ARPABET phonemes of a single word and will return the word converted to 
//          the new alphabet. All ligatures, dummy character expansion and other
//          post-processing is handled in here. These functions will be passed a second
//          paramater denoting capitalization style:
//
//          0 = all lowercase
//          1 = First Letter Of Word Is Capitalized
//          2 = ALL CAPS
//
//          Per the notes below, do not use literal non-latin characters here. Work
//          with the codepoints in decimal notation and convert them to strings using
//          the alpha.utfString() function before returning them. This is the final
//          step to enable a new alphabet.

    // Specific per-alphabet word transliteration functions. They must be explicitly named
    // in order to work with the Mozilla-specific .name property employed above in the 
    // node processing functions for marking up nodes. Anonymous functions have a .name
    // property which is an empty string. 
    
    // The postprocessing functions broke and spat garbage when I moved them out of
    // the the main file, overlay.js. Originally there were literal non-latin
    // characters here, but when I changed up the code to use only codepoints, it
    // worked again. This implies that Firefox's import function can't cope with
    // characters outside the Basic Multilingual plane. As with the character maps,
    // do not use literal non-latin characters here. Unicode codepoints only.

    // Runic font: http://junicode.sourceforge.net/
    // 5801 (Ac) + something = arpabet OW
    // Can't figure out how to pronounse Ēoh
    // latin k + s = eolh (unicode 5833)
    // Yr becomes ur in most English dialects. Yr is unused.
    // Ior = IPA 'jo', as in 'yolk'. Or English 'ia', 'io'.
    // 'ear' is still unused.

    // Shavian font: http://www.alanwood.net/downloads/mph-2b-damase.zip

    // Note to self: all the fonts on unifon.org place their characters in the 
    // in the standard ASCII range instead of the Unicode private-use blocks. That's 
    // very bad. Unifon is already listed in the conscript registry, so the fonts
    // should be updated to point to the correct codepoints.

//
//
// ---------------------------------* Misscelllania *--------------------------------
// Do not pay attention to anything below here, these are various utility functions
// ----------------------------------------------------------------------------------
//
//

    utfString: function(codePoints) {
        var str = '';
        for (var i=0; i < codePoints.length; i++) {
            str = str + alpha.unicode(codePoints[i]);
        }
        return str;
    },


    // Plain String.fromCharCode() cannot handle higher codepoints. All characters are stored in
    // JSON maps as their numeric codepoints as Firefox's JSON parser cannot handle the characters
    // themselves.
    // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/String/fromCharCode
    unicode: function (codePt) {  
        if (codePt > 0xFFFF) {  
            codePt -= 0x10000;  
            return String.fromCharCode(0xD800 + (codePt >> 10), 0xDC00 + (codePt & 0x3FF));  
        }  
        else {  
            return String.fromCharCode(codePt);  
        }  
    },


    //https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/String/charCodeAt
    fixedCharCodeAt: function (str, idx) {  
        idx = idx || 0;  
        var code = str.charCodeAt(idx);  
        var hi, low;  
        if (0xD800 <= code && code <= 0xDBFF) { // High surrogate (could change last hex to 0xDB7F to treat high private surrogates as single characters)  
            hi = code;  
            low = str.charCodeAt(idx+1);  
            if (isNaN(low)) {  
                throw 'High surrogate not followed by low surrogate in fixedCharCodeAt()';  
            }  
            return ((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;  
        }  
        if (0xDC00 <= code && code <= 0xDFFF) { // Low surrogate  
            // We return false to allow loops to skip this iteration since should have already handled high surrogate above in the previous iteration  
            return false;  
        }  
        return code;  
    },  


    arrayEqual: function (a1, a2) {
        if (a1.length != a2.length) return false;
        for (var i=0; i < a1.length; i++) {
            if (a1[i] != a2[i]) return false;
        }
        return true;
    },


    transliterate: function(text, alphabet) {
        var splitText = text.split(/(\w+(?:'\w+)|\W+)/g);
        var result = '';
        var map = alpha.alphaMap[alphabet];

        for (var i = 0; i < splitText.length; i++) {
            var word = splitText[i];

            if (typeof (abbrev = map.abbreviate[word.toUpperCase()]) != 'undefined') {
                ph = abbrev;
            }
            else {
                ph = alpha.latinPhonemes(word, map);
                if (ph.length == 0) {
                    result = result + word;
                    continue;
                }
            }

            // Check capitalization of the original word and pass it along
            // with the array of phonemes to the actual alphabet-specific
            // function.
            var caps = 0;
            if (word == word.toUpperCase()) {
                caps = 2;
            }
            else if (word.charAt(0) == word.charAt(0).toUpperCase()) {
                caps = 1;
            }
            result = result + alpha.transWord(ph, map, caps);
        }
        return result;
    },


    transWord: function(ph, map, caps) {

            // map characters phoneme by phoneme
            var replace, prev = "", result = [];
            for (var i = 0; i < ph.length; i++) {
                var cur = map[ ph[i] ];
                if (typeof cur == 'undefined') return "INVALID";
                if (Array.isArray(cur)) {
                    for (var j = 0; j < cur.length; j++) result.push(cur[j]);
                    prev = cur[j];
                    continue;
                }

                // check for and convert cetain combinations to single characters
                // Only supports two characters at a time for now.
                if (replace = map.compress[ [prev, cur].join('_') ]) {
                    result = result.slice(0, result.length-1); // drop last character
                    cur = replace;                             // this will replace it
                }
                result.push(cur);
                prev = cur;
            }


            // maintain capitalization in alphabets with capital letters.
            if (map.capsOffset) {
                if (caps == 1) {
                    result[0] = result[0] + map.capsOffset;
                }
                else if (caps == 2) {
                    result = result.map(function(x) { return x + map.capsOffset });
                }
            }
            return alpha.utfString(result);
    },


    // TODO: Strip accents from loanwords. (Or see if they can be added into the dictionary in 
    //       uppercase...)
    latinPhonemes: function(word, map) {
        var caps = 0, phonemes = [], candidates;
        if (word.match(/'s$/)) {
            // Manually add sound of 's at the end of words. For obvious
            // reasons, CMUdict's list of words ending in 's is not
            // exhaustive. One wonders why they bothered.
            if (candidates = alpha.lookup(word.slice(0, word.length-2))) {
                phonemes = candidates[0].slice(0);      // .slice(0) = deep copy.
                var lastPhoneme = phonemes[phonemes.length-1];
                if (["P","T","K","F","TH","HH"].indexOf(lastPhoneme) != -1) {
                    phonemes.push("S");
                }
                else if (["CH","JH","S","Z","SH","ZH"].indexOf(lastPhoneme) != -1) {
                    phonemes.push("IH0", "Z");
                }
                else {
                    phonemes.push("Z");
                }
            }

        }
        else if (candidates = alpha.lookup(word)) {
            phonemes = candidates[0];
        }

        return phonemes;
    },

    
    altPhonemes: function(word, map) {
        var result = [];
        for (var i = 0; i < word.length; i++) {
            code = alpha.fixedCharCodeAt(word, i);
            if (!code) continue;
            if (code < map.unicodeStart || map.unicodeEnd < code) continue;
            var ch = map[code];
            if (!ch) continue;
            if (ch.indexOf('_') != -1) {
                result.push(ch.split('_').map(function (a) { return map[a] }));
            }
            else {
                result.push(ch);
            }
        }
        return result;
    },


    reverseCharacterMap: function(map) {
        var newMap = {};
        for (key in map) {
            if (!key.match(/^[A-Z]+$/)) continue; // ignore stress

            // Index unrepresented phonemes by a concatenated string of
            // the unicode characters they expand to.
            if (Array.isArray( map[key] )) {
                newMap[ map[key].join('_') ] = key;
                continue;
            }

            // flip phoneme to character map and store in newMap. Handle capital
            // letters also if they exist.
            newMap[ map[key] ] = key;
            if (map.capsOffset) {
                newMap[ map[key] + map.capsOffset ] = key;
            }
        }
        for (key in map.compress) {
            newMap[ map.compress[key] ] = key;
            if (map.capsOffset) {
                newMap[ map[key] + map.capsOffset ] = key;
            }
        }

        //AH0 (English schwa) is distinct in some alphabets, add it back.
        newMap[ map["AH0"] ] =  "AH0"; 

        newMap.unicodeStart = map.unicodeStart;
        newMap.unicodeEnd   = map.unicodeEnd;
        return newMap;
    }
}

